<!DOCTYPE html>
<html lang="en">

<!-- Head tag -->
<head><meta name="generator" content="Hexo 3.8.0">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="google-site-verification" content="xBT4GhYoi5qRD5tr338pgPM5OWHHIDR6mNg1a3euekI">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="keyword" content="">
    <link rel="shortcut icon" href="/myblog/img/favicon.ico">

    <title>
        
          全栈开发实战：用Vue2+Koa2开发完整的前后端项目 - 郝以利的博客 | inherent&#39;s Blog
        
    </title>

    <link rel="canonical" href="https://inherents.gitee.io/myblog/myblog/2018/11/27/Vue2-Koa2/">

    <!-- Bootstrap Core CSS -->
    <link rel="stylesheet" href="/myblog/css/bootstrap.min.css">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="/myblog/css/hux-blog.min.css">

    <!-- Pygments Highlight CSS -->
    <link rel="stylesheet" href="/myblog/css/highlight.css">

    <!-- Custom Fonts -->
    <!-- <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -->
    <!-- Hux change font-awesome CDN to qiniu -->
    <link href="https://cdn.staticfile.org/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">


    <!-- Hux Delete, sad but pending in China
    <link href='http://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic' rel='stylesheet' type='text/css'>
    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/
    css'>
    -->


    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
        <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->

    <!-- ga & ba script hoook -->
    <script></script>
</head>


<!-- hack iOS CSS :active style -->
<body ontouchstart="">

    <!-- Navigation -->
<nav class="navbar navbar-default navbar-custom navbar-fixed-top">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header page-scroll">
            <button type="button" class="navbar-toggle">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/myblog/">郝以利</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <!-- Known Issue, found by Hux:
            <nav>'s height woule be hold on by its content.
            so, when navbar scale out, the <nav> will cover tags.
            also mask any touch event of tags, unfortunately.
        -->
        <div id="huxblog_navbar">
            <div class="navbar-collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li>
                        <a href="/myblog/">Home</a>
                    </li>

                    

                        
                        <li>
                            <a href="/myblog/about/">About</a>
                        </li>
                        
                    

                        
                        <li>
                            <a href="/myblog/archives/">Archives</a>
                        </li>
                        
                    

                        
                        <li>
                            <a href="/myblog/tags/">Tags</a>
                        </li>
                        
                    
                    
                </ul>
            </div>
        </div>
        <!-- /.navbar-collapse -->
    </div>
    <!-- /.container -->
</nav>
<script>
    // Drop Bootstarp low-performance Navbar
    // Use customize navbar with high-quality material design animation
    // in high-perf jank-free CSS3 implementation
    var $body   = document.body;
    var $toggle = document.querySelector('.navbar-toggle');
    var $navbar = document.querySelector('#huxblog_navbar');
    var $collapse = document.querySelector('.navbar-collapse');

    $toggle.addEventListener('click', handleMagic)
    function handleMagic(e){
        if ($navbar.className.indexOf('in') > 0) {
        // CLOSE
            $navbar.className = " ";
            // wait until animation end.
            setTimeout(function(){
                // prevent frequently toggle
                if($navbar.className.indexOf('in') < 0) {
                    $collapse.style.height = "0px"
                }
            },400)
        }else{
        // OPEN
            $collapse.style.height = "auto"
            $navbar.className += " in";
        }
    }
</script>


    <!-- Main Content -->
    
<!-- Image to hack wechat -->
<!-- <img src="https://inherents.gitee.io/myblog/myblog/img/icon_wechat.png" width="0" height="0"> -->
<!-- <img src="{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}" width="0" height="0"> -->

<!-- Post Header -->
<style type="text/css">
    header.intro-header{
        background-image: url('/myblog/img/home-bg-o.jpg')
    }
</style>
<header class="intro-header">
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <div class="post-heading">
                    <div class="tags">
                        
                    </div>
                    <h1>全栈开发实战：用Vue2+Koa2开发完整的前后端项目</h1>
                    <h2 class="subheading"></h2>
                    <span class="meta">
                        Posted by 郝以利 on
                        2018-11-27
                    </span>
                </div>
            </div>
        </div>
    </div>
</header>

<!-- Post Content -->
<article>
    <div class="container">
        <div class="row">

    <!-- Post Container -->
            <div class="
                col-lg-8 col-lg-offset-2
                col-md-10 col-md-offset-1
                post-container">

                <h1 id="简介"><a href="#简介" class="headerlink" title="简介"></a><strong>简介</strong></h1><p>本文从一名新手的角度（默认对Vue有了解，对Koa或者Express有了解）出发，从0开始构建一个数据通过Koa提供API的形式获取，页面通过Vue渲染的完整的前端项目。可以了解到Vue构建单页面的一些知识以及前端路由的使用、Koa如何提供API接口，如何进行访问过滤（路由）、验证（JSON-WEB-TOKEN）以及Sequelize操作MySQL数据库的一些知识和技巧，希望能够作为一篇入门全栈开发的文章吧。</p>
<p>更新：文末给出的github仓库已经更新Koa2版本。请使用Node.js v7.6.0及以上版本体验~</p>
<hr>
<h1 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a><strong>写在前面</strong></h1><p>我曾经写过一篇文章，是用express和mongodb入门Nodejs的前后端开发，这篇文章里简单的做了一个小demo，能够让你读写mongodb数据库，并且从数据库里将数据读取出来显示到页面上。算是一个简单的读写小demo吧，也算是服务端渲染的一次初尝试。并且我还写过用nodejs写简单小爬虫的文章，用爬虫来获取数据写入数据库。通过以上的的方法我用express写了一个小网站，记录并显示北邮人论坛每天的十大的内容。挺好玩的对吧，可以把想要做的事用代码来实现。</p>
<p>后来我接触到了Koa，并开始了学习，从express迁移到Koa其实曲线还算是比较平滑的。不过用Koa的方式也还是采用服务端渲染页面的方式。而且我发现目前网络上少有写过用Koa构建的前后端分离的应用、网站文章，我最近做的一个项目里需要用到的方式就是用Vue构建页面，数据的获取全部走后端API的形式，也就是所谓的前后端分离吧。正好在这过程中走了不少的坑，包括数据库的使用上也算是个新手，所以写篇文章记录一下，用同样的思路和方法构建一个简单的Todolist，欢迎讨论，轻拍~</p>
<hr>
<h1 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a><strong>项目结构</strong></h1><p>.<br>├── LICENSE<br>├── README.md<br>├── .env  // 环境变量配置文件<br>├── app.js  // Koa入口文件<br>├── build // vue-cli 生成，用于webpack监听、构建<br>│   ├── build.js<br>│   ├── check-versions.js<br>│   ├── dev-client.js<br>│   ├── dev-server.js<br>│   ├── utils.js<br>│   ├── webpack.base.conf.js<br>│   ├── webpack.dev.conf.js<br>│   └── webpack.prod.conf.js<br>├── config // vue-cli 生成&amp;自己加的一些配置文件<br>│   ├── default.conf<br>│   ├── dev.env.js<br>│   ├── index.js<br>│   └── prod.env.js<br>├── dist // Vue build 后的文件夹<br>│   ├── index.html // 入口文件<br>│   └── static // 静态资源<br>├── index.html // vue-cli生成，用于容纳Vue组件的主html文件。单页应用就只有一个html<br>├── package.json // npm的依赖、项目信息文件<br>├── server // Koa后端，用于提供Api<br>│   ├── config // 配置文件夹<br>│   ├── controllers // controller-控制器<br>│   ├── models // model-模型<br>│   ├── routes // route-路由<br>│   └── schema // schema-数据库表结构<br>├── src // vue-cli 生成&amp;自己添加的utils工具类<br>│   ├── App.vue // 主文件<br>│   ├── assets // 相关静态资源存放<br>│   ├── components // 单文件组件<br>│   ├── main.js // 引入Vue等资源、挂载Vue的入口js<br>│   └── utils // 工具文件夹-封装的可复用的方法、功能<br>└── yarn.lock // 用yarn自动生成的lock文件<br>看起来好像很复杂的样子，其实很大一部分文件夹的结构是vue-cli这个工具帮我们生成的。而我们需要额外添加的主要是Koa的入口文件以及一个server文件夹用于Koa提供API。这样的话，在获取数据的方面就可以走Koa所提供的API，而Vue只需关心怎么把这些数据渲染到页面上就好了。</p>
<hr>
<h1 id="项目用到的一些关键依赖"><a href="#项目用到的一些关键依赖" class="headerlink" title="项目用到的一些关键依赖"></a><strong>项目用到的一些关键依赖</strong></h1><p>以下依赖的版本都是本文所写的时候的版本，或者更旧一些</p>
<p>Vue.js(v2.1.8)<br>Vue-Router(v2.1.1)<br>Axios(v0.15.3)<br>Element(v1.1.2)<br>Koa.js(v1.2.4) // 没采用Koa2<br><a href="mailto:Koa-Router@5.4" target="_blank" rel="noopener">Koa-Router@5.4</a>\Koa-jwt\Koa-static等一系列Koa中间件<br>Mysql(v2.12.0) // nodejs的mysql驱动，并不是mysql本身版本（项目采用mysql5.6）<br>Sequelize(v3.28.0) // 操作数据库的ORM<br>Yarn(v0.18.1) // 比起npm更快一些<br>剩下依赖可以参考本文最后给出的项目demo仓库。</p>
<hr>
<h1 id="项目启动"><a href="#项目启动" class="headerlink" title="项目启动"></a><strong>项目启动</strong></h1><p>Nodejs与npm的安装不再叙述（希望大家装上的node版本大于等于6.x，不然还需要加上–harmony标志才可以开启es6），默认读者已经掌握npm安装依赖的方法。首先全局安装npm i vue-cli -g，当然本项目基本上是采用yarn，所以也可以yarn global add vue-cli。</p>
<p>Tips: 可以给yarn换上淘宝源，速度更快: yarn config set registry “<a href="https://registry.npm.taobao.org&quot;" target="_blank" rel="noopener">https://registry.npm.taobao.org&quot;</a></p>
<p>然后我们初始化一个Vue2的webpack的模板：</p>
<p>vue init webpack demo</p>
<p>Tips: 上面的demo可以填写你自己的项目名称</p>
<p>然后进行一些基本配置选择之后，你就可以得到一个基本的vue-cli生成的项目结构。</p>
<p>接着我们进入vue-cli生成的目录，安装Vue的项目依赖并安装Koa的项目依赖：yarn &amp;&amp; yarn add koa <a href="mailto:koa-router@5.4" target="_blank" rel="noopener">koa-router@5.4</a> koa-logger koa-json koa-bodyparser，（注意是安装koa-router的5.4版，因为7.X版本是支持Koa2的）然后进行一些基本目录建立：</p>
<p>在vue-cli生成的demo目录下，建立server文件夹以及子文件夹：</p>
<p>├── server // Koa后端，用于提供Api<br>    ├── config // 配置文件夹<br>    ├── controllers // controller-控制器<br>    ├── models // model-模型<br>    ├── routes // route-路由<br>    └── schema // schema-数据库表结构<br>然后在demo文件夹下我们创建一个app.js的文件，作为Koa的启动文件。</p>
<p>写入如下基本的内容就可以启动Koa啦：</p>
<p>const app = require(‘koa’)()<br>  , koa = require(‘koa-router’)()<br>  , json = require(‘koa-json’)<br>  , logger = require(‘koa-logger’); // 引入各种依赖</p>
<p>app.use(require(‘koa-bodyparser’)());<br>app.use(json());<br>app.use(logger());</p>
<p>app.use(function* (next){<br>  let start = new Date;<br>  yield next;<br>  let ms = new Date - start;<br>  console.log(‘%s %s - %s’, this.method, this.url, ms); // 显示执行的时间<br>});</p>
<p>app.on(‘error’, function(err, ctx){<br>  console.log(‘server error’, err);<br>});</p>
<p>app.listen(8889,() =&gt; {<br>  console.log(‘Koa is listening in 8889’);<br>});</p>
<p>module.exports = app;<br>然后在控制台输入node app.js，能看到输出Koa is listening in 8889，则说明我们的Koa已经启动成功了，并在8889端口监听。</p>
<hr>
<h1 id="前端页面构建"><a href="#前端页面构建" class="headerlink" title="前端页面构建"></a><strong>前端页面构建</strong></h1><p>这个DEMO是做一个Todo-List，我们首先来做一个登录页面。</p>
<p>Tips: 为了方便构建页面和美观，本文采用的Vue2的前端UI框架是element-ui。安装：yarn add element-ui</p>
<p>模板引擎我习惯用pug，CSS预处理我习惯用stylus，当然每个人自己的习惯和喜好是不一样的，所以大家根据自己平时的喜好来就行了。</p>
<p>为了方便大家查看代码，就不用pug了，学习成本相对高一些。不过CSS用stylus写起来简便，看起来也不会难懂，是我自己的习惯，所以还需要安装一下yarn add stylus stylus-loader。</p>
<p>Tips: 安装stylus-loader是为了让webpack能够渲染stylus</p>
<p>然后要把element-ui引入项目中。打开src/main.js，将文件改写如下：</p>
<p>import Vue from ‘vue’<br>import App from ‘./App’<br>import ElementUI from ‘element-ui’ // 引入element-ui<br>import ‘element-ui/lib/theme-default/index.css’</p>
<p>Vue.use(ElementUI) // Vue全局使用</p>
<p>new Vue({<br>  el: ‘#app’,<br>  template: ‘<app>‘,<br>  components: { App }<br>})<br>然后我们在项目根目录下输入npm run dev，启动开发模式，这个模式有webpack的热加载，也就是你写完代码，浏览器立即就能响应变化。</app></p>
<p>为了实现响应式页面，我们要在项目目录下的index.html的head标签内加入以下meta：</p>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

<p>登录界面<br>进入src/components目录，新建一个Login.vue的文件。然后我们来写第一个页面：</p>
<template><br>  <el-row class="content"><br>    <el-col :xs="24" :sm="{span: 6,offset: 9}"><br>      <span class="title"><br>       欢迎登录<br>      </span><br>      <el-row><br>        <el-input v-model="account" placeholder="账号" type="text"><br>        </el-input><br>        <el-input v-model="password" placeholder="密码" type="password"><br>        </el-input><br>        <el-button type="primary">登录</el-button><br>      </el-row><br>    </el-col><br>  </el-row><br></template>

<script>
export default {
  data () {
    return {
      account: '',
      password: ''
    };
  }
};
</script>

<p><style lang="stylus" scoped><br>  .el-row.content<br>    padding 16px<br>  .title<br>    font-size 28px<br>  .el-input<br>    margin 12px 0<br>  .el-button<br>    width 100%<br>    margin-top 12px<br></style><br>在这里就有一些值得注意的地方。首先是template标签内的直接子元素最多只能挂载一个。也就是你不能这么写：</p>
<template><br>  <el-row></el-row><br>  <el-row></el-row><br></template><br>否则会报错：template syntax error Component template should contain exactly one root element，template下只能有一个根元素。不过为了写多个元素，你可以这样：<br><br><br><template><br>  <div><br>    <el-row></el-row><br>    <el-row></el-row><br>  </div><br></template><br>同时注意到，在Login.vue的style标签内有个scoped属性，这个属性能够使这些样式只在这个组件内生效（因为Webpack在渲染的时候会将这个组件内的元素自动打上一串形如data-v-62a7f97e这样的属性，对于这些样式也会变成形如.title[data-v-62a7f97e]{ font-size: 28px;}的样子，保证了不会和其他组件的样式冲突。<br><br>页面写完之后，如果不把组件注册到Vue之下那么页面是不会显示的。因此这个时候需要把APP.vue这个文件改写一下：<br><br><template><br>  <div id="app"><br>    <img src="./assets/logo.png"><br>    <login></login> <!--使用Login组件--><br>  </div><br></template>

<script>
import Login from './components/Login' // 引入Login组件

export default {
  name: 'app',
  components: {
    Login // 注册组件
  }
}
</script>

<p><style></p>
<p>#app {<br>  font-family: ‘Avenir’, Helvetica, Arial, sans-serif;<br>  -webkit-font-smoothing: antialiased;<br>  -moz-osx-font-smoothing: grayscale;<br>  text-align: center;<br>  color: #2c3e50;<br>  margin-top: 60px;<br>}<br></style><br>也就是把Login这个组件注册到Vue下，同时你再看浏览器，已经不再是vue-cli默认生成的Hello欢迎界面了。</p>
<p>Login<br>Login</p>
<p>接着我们写一下登录成功后的界面。</p>
<p>TodoList页面<br>还是在src/components目录下，写一个叫做TodoList.vue的文件。</p>
<p>接着我们开始写一个TodoList：</p>
<template><br>  <el-row class="content"><br>    <el-col :xs="{span:20,offset:2}" :sm="{span:8,offset:8}"><br>      <span><br>        欢迎：！你的待办事项是：<br>      </span><br>      <el-input placeholder="请输入待办事项" v-model="todos" @keyup.enter.native="addTodos"></el-input><br>      <el-tabs v-model="activeName"><br>        <el-tab-pane label="待办事项" name="first"><br>          <el-col :xs="24"><br>            <template v-if="!Done"> <!--v-if和v-for不能同时在一个元素内使用，因为Vue总会先执行v-for--><br>              <template v-for="(item, index) in list"><br>                <div class="todo-list" v-if="item.status == false"><br>                  <span class="item"><br>                    NaN. <br>                  </span><br>                  <span class="pull-right"><br>                    <el-button size="small" type="primary" @click="finished(index)">完成</el-button><br>                    <el-button size="small" :plain="true" type="danger" @click="remove(index)">删除</el-button><br>                  </span><br>                </div><br>              </template><br>            </template><br>            <div v-else-if="Done"><br>              暂无待办事项<br>            </div><br>          </el-col><br>        </el-tab-pane><br>        <el-tab-pane label="已完成事项" name="second"><br>          <template v-if="count > 0"><br>            <template v-for="(item, index) in list"><br>              <div class="todo-list" v-if="item.status == true"><br>                <span class="item finished"><br>                  NaN. <br>                </span><br>                <span class="pull-right"><br>                  <el-button size="small" type="primary" @click="restore(index)">还原</el-button><br>                </span><br>              </div><br>            </template><br>          </template><br>          <div v-else=""><br>            暂无已完成事项<br>          </div><br>        </el-tab-pane><br>      </el-tabs><br>    </el-col><br>  </el-row><br></template>

<script>
export default {
  data () {
    return {
      name: 'Molunerfinn',
      todos: '',
      activeName: 'first',
      list:[],
      count: 0
    };
  },
  computed: { // 计算属性用于计算是否已经完成了所有任务
    Done(){
      let count = 0;
      let length = this.list.length;
      for(let i in this.list){
        this.list[i].status == true ? count += 1 : '';
      }
      this.count = count;
      if(count == length || length == 0){
        return true
      }else{
        return false
      }
    }
  },

  methods: {
    addTodos() {
      if(this.todos == '')
        return
      let obj = {
        status: false,
        content: this.todos
      }
      this.list.push(obj);
      this.todos = '';
    },
    finished(index) {
      this.$set(this.list[index],'status',true) // 通过set的方法让数组的变动能够让Vue检测到
      this.$message({
        type: 'success',
        message: '任务完成'
      })
    },
    remove(index) {
      this.list.splice(index,1);
      this.$message({
        type: 'info',
        message: '任务删除'
      })
    },
    restore(index) {
      this.$set(this.list[index],'status',false)
      this.$message({
        type: 'info',
        message: '任务还原'
      })
    }
  }
};
</script>

<p><style lang="stylus" scoped><br>  .el-input<br>    margin 20px auto<br>  .todo-list<br>    width 100%<br>    margin-top 8px<br>    padding-bottom 8px<br>    border-bottom 1px solid #eee<br>    overflow hidden<br>    text-align left<br>    .item<br>      font-size 20px<br>      &amp;.finished<br>        text-decoration line-through<br>        color #ddd<br>  .pull-right<br>    float right<br></style><br>页面构建其实没有什么特别好说的，但是因为我自己有踩点坑，所以还是专门讲一下：</p>
<p>v-if和v-for放在一个元素内同时使用，因为Vue总会先执行v-for，所以导致v-if不会被执行。替代地，你可以使用一个额外的template元素用来放置v-if或者v-for从而达到同样的目的。这是相关的issue。</p>
<p>计算属性对于直接的数据比如a: 2 -&gt; a: 3这样的数据变动可以直接检测到。但是如果是本例中的list的某一项的status这个属性变化了，如果我们直接使用list[index].status = true这样的写法的话，Vue将无法检测到数据变动。替代地，可以使用set方法（全局是Vue.set()，实例中是this.$set()），通过set方法可以让数据的变动变得可以被检测到。从而让计算属性能够捕捉到变化。可以参考官方文档对于响应式原理的描述。</p>
<p>Todolist<br>Todolist</p>
<p>写完TodoList之后，我们需要将它和vue-router配合起来，从而使这个单页应用能够进行页面跳转。</p>
<p>页面路由<br>由于不采用服务端渲染，所以页面路由走的是前端路由。安装一下vue-router：yarn add vue-router。</p>
<p>安装好后，我们挂载一下路由。打开main.js文件改写如下：</p>
<p>// src/main.js</p>
<p>import Vue from ‘vue’<br>import App from ‘./App’<br>import ElementUI from ‘element-ui’<br>import ‘element-ui/lib/theme-default/index.css’<br>import VueRouter from ‘vue-router’</p>
<p>Vue.use(ElementUI);<br>Vue.use(VueRouter);</p>
<p>import Login from <code>./components/Login</code><br>import TodoList from <code>./components/TodoList</code></p>
<p>const router =  new VueRouter({<br>  mode: ‘history’, // 开启HTML5的history模式，可以让地址栏的url长得跟正常页面跳转的url一样。（不过还需要后端配合，讲Koa的时候会说）<br>  base: __dirname,<br>  routes: [<br>    {<br>      path: ‘/‘,  // 默认首页打开是登录页<br>      component: Login<br>    },<br>    {<br>      path: ‘/todolist’,<br>      component: TodoList<br>    },<br>    {<br>      path: ‘*’,<br>      redirect: ‘/‘ // 输入其他不存在的地址自动跳回首页<br>    }<br>  ]<br>})</p>
<p>const app = new Vue({<br>  router: router, // 启用router<br>  render: h =&gt; h(App)<br>}).$mount(‘#app’) //挂载到id为app的元素上<br>这样就把路由挂载好了，但是你打开页面发现好像还是没有什么变化。这是因为我们没有把路由视图放到页面上。现在我们改写一下APP.vue：</p>
<!-- APP.vue -->
<template><br>  <div id="app"><br>    <img src="./assets/logo.png"><br>    <router-view></router-view> <!-- 原本的Login换成了router-view 这就是路由视图渲染的目标元素--><br>  </div><br></template>

<script>
export default {
  name: 'app' // 不需要再引入`Login`\`TodoList`组件了，因为在路由里已经注册了
}
</script>

<p><style></p>
<p>#app {<br>  font-family: ‘Avenir’, Helvetica, Arial, sans-serif;<br>  -webkit-font-smoothing: antialiased;<br>  -moz-osx-font-smoothing: grayscale;<br>  text-align: center;<br>  color: #2c3e50;<br>  margin-top: 60px;<br>}<br></style><br>然后再看一下你的页面，这个时候你如果在地址栏后加上/todolist那么就会跳转到TodoList页面啦。</p>
<p>不过我们如何通过点击登录按钮跳转到TodoList呢？改写一下Login.vue，就可以跳转了。</p>
<p>只需要给登录的button加一个方法即可：</p>
<!-- Login.vue -->
<p>······</p>
<!-- 给input增加键盘事件，当输入完密码回车也执行loginToDo方法 -->
<p><el-input v-model="password" placeholder="密码" type="password" @keyup.enter.native="loginToDo"><br></el-input><br><!-- 增加一个click方法 loginToDo --></p>
<p><el-button type="primary" @click="loginToDo">登录</el-button></p>
<p>······</p>
<p><script><br>export default {<br>  data () {<br>    return {<br>      account: ‘’,<br>      password: ‘’<br>    };<br>  },<br>  methods: {<br>    loginToDo() {<br>      this.$router.push(‘/todolist’) // 编程式路由，通过push方法，改变路由。<br>    }<br>  }<br>};<br></script><br>然后你就可以通过点击登录按钮进行页面跳转了。并且你可以发现，页面地址从localhost:8080变成了localhost:8080/todolist，长得跟正常的url跳转一样。（但是实际上我们是单页应用，只是在应用内进行页面跳转而已，没有向后端额外请求）</p>
<p>login2todolist<br>login2todolist</p>
<p>至此，我们已经完成了一个纯前端的单页应用，能够进行页面跳转，能够做简单的ToDoList的添加和删除和还原。当然这个东西只能算是个能看不能用的东西——因为登录系统有名无实、ToDoList只要页面刷新一下就没了。</p>
<p>于是我们可以先把前端放一放。开启我们的后端之旅。</p>
<hr>
<h1 id="后端环境搭建"><a href="#后端环境搭建" class="headerlink" title="后端环境搭建"></a><strong>后端环境搭建</strong></h1><p>MySQL<br>之所以没有用Node界大家普遍喜爱的Mongodb主要是因为之前我用过它，而没有用过MySQL，本着学习的态度，我决定用用MySQL。还有就是Express + Mongodb的教程其实很早之前就已经满大街都是了。所以如果你觉得Mongodb更合你的胃口，看完本文你完全可以用Mongodb构建一个类似的应用。</p>
<p>去MySQL的官网下载安装对应平台MySQL的Community Server。</p>
<p>通常来说安装的步骤都是比较简单的。对于MySQL的基本安装、开启步骤可以参考这篇文章，这篇是windows的。当然其他平台的安装也是很方便的，都有相应的包管理工具可以获取。值得注意的就是，安装完MySQL之后你需要设定一下root账户的密码。保证安全性。如果你漏了设定，或者你不知道怎么设定，可以参考这篇文章</p>
<p>因为我对MySQL的SQL语句不是很熟悉，所以我需要一个可视化的工具来操作MySQL。Windows上我用的是HediSQL，macOS上我用的是Sequel Pro。它们都是免费的。</p>
<p>然后我们可以用这些可视化工具连上MySQL的server（默认端口是3306）之后，创建一个新的数据库，叫做todolist。（当然你也可以用SQL语句:CREATE DATABASE todolist，之后不再赘述）。</p>
<p>接着我们可以来开始创建数据表了。</p>
<p>我们需要创建两张表，一张是用户表，一张是待办事项表。用户表用于登录、验证，待办事项表用于展示我们的待办事项。</p>
<p>创建一张user表，其中password我们稍后会进行bcrypt加密（取128位）。</p>
<p>字段    类型    说明<br>id    int（自增）    用户的id<br>user_name    CHAR(50)    用户的名字<br>password    CHAR(128)    用户的密码<br>创建一张list表，所需的字段是id、user_id、content、status即可。</p>
<p>字段    类型    说明<br>id    int（自增）    list的id<br>user_id    int(11)    用户的id<br>content    CHAR(255)    list的内容<br>status    tinyint(1)    list的状态<br>直接跟数据库打交道的部分基本就是这样了。</p>
<p>Sequelize<br>跟数据库打交道的时候我们都需要一个好的操作数据库的工具，能够让我们用比较简单的方法来对数据库进行增删改查。对于Mongodb来说大家熟悉的是Mongoose以及我用过一个相对更简单点的Monk。对于MySQL，我选用的是Sequelize，它支持多种关系型数据库（Sqlite、MySQL、Postgres等），它的操作基本都能返回一个Promise对象，这样在Koa里面我们能够很方便地进行”同步”操作。</p>
<p>更多关于Sequelize的用法，可以参考官方文档，以及这两篇文章——Sequelize中文API文档、Sequelize和MySQL对照</p>
<p>在用Sequelize连接数据库之前我们需要把数据库的表结构用sequelize-auto导出来。</p>
<p>更多关于sequelize-auto的使用可以参考官方介绍或者这篇文章</p>
<p>由此我们需要分别安装这几个依赖：yarn global add sequelize-auto &amp;&amp; yarn add sequelize mysql。</p>
<p>注：上面用yarn安装的mysql是nodejs环境下的mysql驱动。</p>
<p>进入server的目录，执行如下语句sequelize-auto -o “./schema” -d todolist -h 127.0.0.1 -u root -p 3306 -x XXXXX -e mysql，（其中 -o 参数后面的是输出的文件夹目录， -d 参数后面的是数据库名， -h 参数后面是数据库地址， -u 参数后面是数据库用户名， -p 参数后面是端口号， -x 参数后面是数据库密码，这个要根据自己的数据库密码来！ -e 参数后面指定数据库为mysql）</p>
<p>然后就会在schema文件夹下自动生成两个文件：</p>
<p>// user.js</p>
<p>module.exports = function(sequelize, DataTypes) {<br>  return sequelize.define(‘user’, {<br>    id: {<br>      type: DataTypes.INTEGER(11), // 字段类型<br>      allowNull: false, // 是否允许为NULL<br>      primaryKey: true, // 主键<br>      autoIncrement: true // 是否自增<br>    },<br>    user_name: {<br>      type: DataTypes.CHAR(50), // 最大长度为50的字符串<br>      allowNull: false<br>    },<br>    password: {<br>      type: DataTypes.CHAR(32),<br>      allowNull: false<br>    }<br>  }, {<br>    tableName: ‘user’ // 表名<br>  });<br>};<br>// list.js</p>
<p>module.exports = function(sequelize, DataTypes) {<br>  return sequelize.define(‘list’, {<br>    id: {<br>      type: DataTypes.INTEGER(11),<br>      allowNull: false,<br>      primaryKey: true,<br>      autoIncrement: true<br>    },<br>    user_id: {<br>      type: DataTypes.INTEGER(11),<br>      allowNull: false<br>    },<br>    content: {<br>      type: DataTypes.CHAR(255),<br>      allowNull: false<br>    },<br>    status: {<br>      type: DataTypes.INTEGER(1),<br>      allowNull: false<br>    }<br>  }, {<br>    tableName: ‘list’<br>  });<br>};<br>自动化工具省去了很多我们手动定义表结构的时间。同时注意到生成的数据库表结构文件都自动帮我们module.exports出来了，所以很方便我们之后的引入。</p>
<p>在server目录下的config目录下我们新建一个db.js，用于初始化Sequelize和数据库的连接。</p>
<p>// db.js</p>
<p>const Sequelize = require(‘sequelize’); // 引入sequelize</p>
<p>// 使用url连接的形式进行连接，注意将root: 后面的XXXX改成自己数据库的密码<br>const Todolist = new Sequelize(‘mysql://root:XXXX@localhost/todolist’,{<br>  define: {<br>    timestamps: false // 取消Sequelzie自动给数据表加入时间戳（createdAt以及updatedAt）<br>  }<br>}) </p>
<p>module.exports = {<br>  Todolist // 将Todolist暴露出接口方便Model调用<br>}<br>接着我们去models文件夹里将数据库和表结构文件连接起来。在这个文件夹下新建一个user.js的文件。我们先来写一个查询用户id的东西。</p>
<p>为此我们可以先在数据库里随意加一条数据：</p>
<p>test<br>test</p>
<p>通常我们要查询一个用户id为1的数据，会很自然的想到类似如下的写法：</p>
<p>const userInfo = User.findOne({ where: { id: 1} }); // 查询<br>console.log(userInfo); // 输出结果<br>但是上面的写法实际上是行不通的。因为JS的特性让它的IO操作是异步的。而上面的写法，userInfo将是返回的一个Promise对象，而不是最终的userInfo。如果又想用同步的写法获取异步IO操作得到的数据的话，通常情况下是不能直接得到的。但是在Koa里，由于有co的存在，让这一切变得十分简单。改写如下：</p>
<p>// models/user.js<br>const db = require(‘../config/db.js’),<br>      userModel = ‘../schema/user.js’; // 引入user的表结构<br>const TodolistDb = db.Todolist; // 引入数据库</p>
<p>const User = TodolistDb.import(userModel); // 用sequelize的import方法引入表结构，实例化了User。</p>
<p>const getUserById = function<em> (id){ // 注意是function</em> 而不是function 对于需要yield操作的函数都需要这种generator函数。<br>  const userInfo = yield User.findOne({ // 用yield控制异步操作，将返回的Promise对象里的数据返回出来。也就实现了“同步”的写法获取异步IO操作的数据<br>    where: {<br>      id: id<br>    }<br>  });</p>
<p>  return userInfo // 返回数据<br>}</p>
<p>module.exports = {<br>  getUserById  // 导出getUserById的方法，将会在controller里调用<br>}<br>接着我们在controllers写一个user的controller，来执行这个方法，并返回结果。</p>
<p>// controllers/user.js </p>
<p>const user = require(‘../models/user.js’);</p>
<p>const getUserInfo = function* (){<br>  const id = this.params.id; // 获取url里传过来的参数里的id<br>  const result = yield user.getUserById(id);  // 通过yield “同步”地返回查询结果<br>  this.body = result // 将请求的结果放到response的body里返回<br>}</p>
<p>module.exports = {<br>  getUserInfo // 把获取用户信息的方法暴露出去<br>}<br>写完这个还不能直接请求，因为我们还没有定义路由，请求经过Koa找不到这个路径是没有反应的。</p>
<p>在routes文件夹下写一个auth.js的文件。（其实user表是用于登录的，所以走auth）</p>
<p>// routes/auth.js</p>
<p>const auth = require(‘../controllers/user.js’);<br>const router = require(‘koa-router’)();</p>
<p>router.get(‘/user/:id’, auth.getUserInfo); // 定义url的参数是id,用user的auth方法引入router</p>
<p>module.exports = router; // 把router规则暴露出去<br>至此我们已经接近完成我们的第一个API了，还缺最后一步，将这个路由规则“挂载”到Koa上去。</p>
<p>回到根目录的app.js，改写如下：</p>
<p>const app = require(‘koa’)()<br>  , koa = require(‘koa-router’)()<br>  , json = require(‘koa-json’)<br>  , logger = require(‘koa-logger’)<br>  , auth = require(‘./server/routes/auth.js’); // 引入auth</p>
<p>app.use(require(‘koa-bodyparser’)());<br>app.use(json());<br>app.use(logger());</p>
<p>app.use(function* (next){<br>  let start = new Date;<br>  yield next;<br>  let ms = new Date - start;<br>  console.log(‘%s %s - %s’, this.method, this.url, ms);<br>});</p>
<p>app.on(‘error’, function(err, ctx){<br>  console.log(‘server error’, err);<br>});</p>
<p>koa.use(‘/auth’, auth.routes()); // 挂载到koa-router上，同时会让所有的auth的请求路径前面加上’/auth’的请求路径。</p>
<p>app.use(koa.routes()); // 将路由规则挂载到Koa上。</p>
<p>app.listen(8889,() =&gt; {<br>  console.log(‘Koa is listening in 8889’);<br>});</p>
<p>module.exports = app;<br>打开你的控制台，输入node app.js，一切运行正常没有报错的话，大功告成，我们的第一个API已经构建完成！</p>
<p>如何测试呢？</p>
<p>API Test<br>接口在跟跟前端对接之前，我们应该先进行一遍测试，防止出现问题。在测试接口的工具上，我推荐Postman，这个工具能够很好的模拟发送的各种请求，方便的查看响应结果，用来进行测试是最好不过了。</p>
<p>Postman<br>Postman</p>
<p>测试成功，我发送了正确的url请求，返回的结果也是我想看到的。我们看到返回的结果实际上是个JSON，这对于我们前后端来说都是十分方便处理的数据格式。</p>
<p>但是如果我们代码出了问题，返回error了我们该怎么测试呢？如果说控制台能够反馈一定的信息，但是绝对不充分，并且我们很可能不知道哪步出错了导致最终结果出问题。</p>
<p>所以我推荐用VSCode这个工具来帮我们调试nodejs后端的代码。它能够添加断点，能够很方便地查看请求的信息。并且配合上nodemon这类的工具，调试简直不要更舒服。</p>
<p>关于VSCode的nodejs调试，可以参考官方的这篇文章</p>
<p>我自己是用Sublime写代码，用VSCode调试，哈哈。</p>
<p>登录系统的实现<br>刚才实现的不过是一个简单的用户信息查询的接口，但是我们要实现的是一个登录系统，所以还需要做一些工作。</p>
<p>JSON-WEB-TOKEN<br>基于cookie或者session的登录验证已经屡见不鲜，前段时间JSON-WEB-TOKEN出来后很是风光了一把。引入了它之后，能够实现真正无状态的请求，而不是基于session和cookie的存储式的有状态验证。</p>
<p>关于JSON-WEB-TOKEN的描述可以参考这篇文章比较简单，我还推荐一篇文章，将如何使用JSON-WEB-TOKEN写得很清楚。</p>
<p>另外可以在JSON-WEB-TOKEN的官网上感受一下。</p>
<p>Tips：JSON-WEB-TOKEN分三部分，头部信息+主体信息+密钥信息，其中主体传递的信息（是我们存放我们需要的信息的部分）是用BASE64编码的，所以很容易被解码，一定不能存放明文密码这种关键信息！替代地可以存放一些不是特别关键的信息，比如用户名这样能够做区分的信息。</p>
<p>简单来说，运用了JSON-WEB-TOKEN的登录系统应该是这样的：</p>
<p>用户在登录页输入账号密码，将账号密码（密码进行md5加密）发送请求给后端<br>后端验证一下用户的账号和密码的信息，如果符合，就下发一个TOKEN返回给客户端。如果不符合就不发送TOKEN回去，返回验证错误信息。<br>如果登录成功，客户端将TOKEN用某种方式存下来（SessionStorage、LocalStorage）,之后要请求其他资源的时候，在请求头（Header）里带上这个TOKEN进行请求。<br>后端收到请求信息，先验证一下TOKEN是否有效，有效则下发请求的资源，无效则返回验证错误。<br>通过这个TOKEN的方式，客户端和服务端之间的访问，是无状态的：也就是服务端不知道你这个用户到底还在不在线，只要你发送的请求头里的TOKEN是正确的我就给你返回你想要的资源。这样能够不占用服务端宝贵的空间资源，而且如果涉及到服务器集群，如果服务器进行维护或者迁移或者需要CDN节点的分配的话，无状态的设计显然维护成本更低。</p>
<p>话不多说，我们来把JSON-WEB-TOKEN用到我们的项目中。</p>
<p>yarn add koa-jwt，安装Koa的JSON-WEB-TOKEN库。</p>
<p>我们需要在models里的user.js加一个方法，通过用户名查找用户：</p>
<p>// models/user.js<br>// ……<br>// 前面的省略了</p>
<p>// 新增一个方法，通过用户名查找<br>const getUserByName = function* (name){<br>  const userInfo = yield User.findOne({<br>    where: {<br>      user_name: name<br>    }<br>  })</p>
<p>  return userInfo<br>}</p>
<p>module.exports = {<br>  getUserById, // 导出getUserById的方法，将会在controller里调用<br>  getUserByName<br>}<br>然后我们写一下controllers里的user.js：</p>
<p>// controllers/user.js</p>
<p>const user = require(‘../models/user.js’);<br>const jwt = require(‘koa-jwt’); // 引入koa-jwt</p>
<p>const getUserInfo = function* (){<br>  const id = this.params.id; // 获取url里传过来的参数里的id<br>  const result = yield user.getUserById(id);  // 通过yield “同步”地返回查询结果<br>  this.body = result // 将请求的结果放到response的body里返回<br>}</p>
<p>const postUserAuth = function* (){<br>  const data = this.request.body; // post过来的数据存在request.body里<br>  const userInfo = yield user.getUserByName(data.name);</p>
<p>  if(userInfo != null){ // 如果查无此用户会返回null<br>    if(userInfo.password != data.password){<br>      this.body = {<br>        success: false, // success标志位是方便前端判断返回是正确与否<br>        info: ‘密码错误！’<br>      }<br>    }else{ // 如果密码正确<br>      const userToken = {<br>        name: userInfo.user_name,<br>        id: userInfo.id<br>      }<br>      const secret = ‘vue-koa-demo’; // 指定密钥，这是之后用来判断token合法性的标志<br>      const token = jwt.sign(userToken,secret); // 签发token<br>      this.body = {<br>        success: true,<br>        token: token, // 返回token<br>      }<br>    }<br>  }else{<br>    this.body = {<br>      success: false,<br>      info: ‘用户不存在！’ // 如果用户不存在返回用户不存在<br>    }<br>  }<br>}</p>
<p>module.exports = {<br>  getUserInfo,<br>  postUserAuth<br>}<br>再把routes里的路由规则更新一下：</p>
<p>// routes/auth.js</p>
<p>const auth = require(‘../controllers/user.js’);<br>const router = require(‘koa-router’)();</p>
<p>router.get(‘/user/:id’, auth.getUserInfo); // 定义url的参数是id,用user的auth方法引入router<br>router.post(‘/user’, auth.postUserAuth);</p>
<p>module.exports = router; // 把router规则暴露出去<br>由此我们写完了用户认证的部分。接下去我们要改写一下前端登录的方法。</p>
<p>引入Axios<br>之前在学Vue的时候一直用的是vue-resource，不过后来Vue2出来之后，Vue官方不再默认推荐它为官方的ajax网络请求库了。替代地推荐了一些其他的库，比如就有我们今天要用的axios。我之前也没有用过它，不过看完它的star和简要介绍Promise based HTTP client for the browser and node.js，能够同时支持node和浏览器端的ajax请求工具（还是基于Promised的！），我想就有必要用一用啦。</p>
<p>yarn add axios，安装axios。然后我们在src/main.js里面引入axios：</p>
<p>// scr/main.js</p>
<p>// …</p>
<p>import Axios from ‘axios’</p>
<p>Vue.prototype.$http = Axios // 类似于vue-resource的调用方法，之后可以在实例里直接用this.$http.get()等</p>
<p>// …<br>// Login.vue<br>// 省略前面的部分</p>
<p> methods: {<br>    loginToDo() {<br>      let obj = {<br>        name: this.account,<br>        password: this.password<br>      }<br>      this.$http.post(‘/auth/user’, obj) // 将信息发送给后端<br>        .then((res) =&gt; { // axios返回的数据都在res.data里<br>          if(res.data.success){ // 如果成功<br>            sessionStorage.setItem(‘demo-token’,res.data.token); // 用sessionStorage把token存下来<br>            this.$message({ // 登录成功，显示提示语<br>              type: ‘success’,<br>              message: ‘登录成功！’<br>            });<br>            this.$router.push(‘/todolist’) // 进入todolist页面，登录成功<br>          }else{<br>            this.$message.error(res.data.info); // 登录失败，显示提示语<br>            sessionStorage.setItem(‘demo-token’,null); // 将token清空<br>          }<br>        }, (err) =&gt; {<br>            this.$message.error(‘请求错误！’)<br>            sessionStorage.setItem(‘demo-token’,null); // 将token清空<br>        })<br>    }<br>  }<br>密码bcrypt加密<br>最早的时候我是在前端用了md5加密，但是后来经过提醒这种方式并不安全。md5加密的容易被破解。所以就采用了bcrypt的加密方式。全部走后端加密。也许你会问这样明文给后端发送密码安全吗？没问题，只要用上HTTPS，这将不是问题。</p>
<p>yarn add bcryptjs安装bcryptjs。</p>
<p>// controllers/user.js</p>
<p>const user = require(‘../models/user.js’);<br>const jwt = require(‘koa-jwt’); // 引入koa-jwt<br>const bcrypt = require(‘bcryptjs’);</p>
<p>const getUserInfo = function* (){<br>  const id = this.params.id; // 获取url里传过来的参数里的id<br>  const result = yield user.getUserById(id);  // 通过yield “同步”地返回查询结果<br>  this.body = result // 将请求的结果放到response的body里返回<br>}</p>
<p>const postUserAuth = function* (){<br>  const data = this.request.body; // post过来的数据存在request.body里<br>  const userInfo = yield user.getUserByName(data.name);</p>
<p>  if(userInfo != null){ // 如果查无此用户会返回null<br>    if(!bcrypt.compareSync(data.password, userInfo.password)){ // 验证密码是否正确<br>      this.body = {<br>        success: false, // success标志位是方便前端判断返回是正确与否<br>        info: ‘密码错误！’<br>      }<br>    }else{ // 如果密码正确<br>      const userToken = {<br>        name: userInfo.user_name,<br>        id: userInfo.id<br>      }<br>      const secret = ‘vue-koa-demo’; // 指定密钥，这是之后用来判断token合法性的标志<br>      const token = jwt.sign(userToken,secret); // 签发token<br>      this.body = {<br>        success: true,<br>        token: token, // 返回token<br>      }<br>    }<br>  }else{<br>    this.body = {<br>      success: false,<br>      info: ‘用户不存在！’ // 如果用户不存在返回用户不存在<br>    }<br>  }<br>}</p>
<p>module.exports = {<br>  getUserInfo,<br>  postUserAuth<br>}<br>因为我们数据库里还是存着明文的123作为密码，现在要先将它bcrypt化，加密后变为：$2a$10$x3f0Y2SNAmyAfqhKVAV.7uE7RHs3FDGuSYw.LlZhOFoyK7cjfZ.Q6，将其替换掉数据库里的123。不做这步我们将无法登录。</p>
<p>还没有大功告成，因为我们的界面跑在8080端口，但是Koa提供的API跑在8889端口，所以如果直接通过/auth/user这个url去post是请求不到的。就算写成localhost:8889/auth/user也会因为跨域问题导致请求失败。</p>
<p>这个时候有两种最方便的解决办法：</p>
<p>如果是跨域，服务端只要在请求头上加上CORS，客户端即可跨域发送请求。<br>变成同域，即可解决跨域请求问题。<br>第一种也很方便，采用kcors即可解决。<br>不过为了之后部署方便，我们采用第二种，变成同域请求。</p>
<p>打开根目录下的config/index.js，找到dev下的proxyTable，利用这个proxyTable我们能够将外部的请求通过webpack转发给本地，也就能够将跨域请求变成同域请求了。</p>
<p>将proxyTable改写如下:</p>
<p> proxyTable: {<br>  ‘/auth’:{<br>    target: ‘<a href="http://localhost:8889&#39;" target="_blank" rel="noopener">http://localhost:8889&#39;</a>,<br>    changeOrigin: true<br>  },<br>  ‘/api’:{<br>    target: ‘<a href="http://localhost:8889&#39;" target="_blank" rel="noopener">http://localhost:8889&#39;</a>,<br>    changeOrigin: true<br>  }<br>}<br>上面的意思是，我们在组件里请求的地址如果是/api/xxxx实际上请求的是<a href="http://localhost:8889/api/xxxx，但是由于webpack帮我们代理了localhost的8889端口的服务，所以我们可以把实际是跨域的请求当做是同域下的接口来调用。" target="_blank" rel="noopener">http://localhost:8889/api/xxxx，但是由于webpack帮我们代理了localhost的8889端口的服务，所以我们可以把实际是跨域的请求当做是同域下的接口来调用。</a></p>
<p>此时重新启动一下webpack：先ctrl+c退出当前进程，然后npm run dev。</p>
<p>一切都万事了之后，我们可以看到如下激动人心的画面：</p>
<p>login2todolist<br>login2todolist</p>
<p>跳转拦截<br>虽然我们现在能够成功登录系统了，但是还是存在一个问题：我在地址栏手动将地址改为localhost:8080/todolist我还是能够成功跳转到登录后的界面啊。于是这就需要一个跳转拦截，当没有登录的时候，不管地址栏输入什么地址，最终都重新定向回登录页。</p>
<p>这个时候，从后端给我们传回来的token就派上大用处。有token就说明我们的身份是经过验证的，否则就是非法的。</p>
<p>vue-router提供了页面跳转的钩子，我们可以在router跳转前进行验证，如果token存在就跳转，如果不存在就返回登录页。</p>
<p>参考路由的导航钩子</p>
<p>打开src/main.js，修改如下：</p>
<p>// src/main.js</p>
<p>// …</p>
<p>const router = new VueRouter({….}) // 省略</p>
<p>router.beforeEach((to,from,next) =&gt;{<br>  const token = sessionStorage.getItem(‘demo-token’);<br>  if(to.path == ‘/‘){ // 如果是跳转到登录页的<br>    if(token != ‘null’ &amp;&amp; token != null){<br>      next(‘/todolist’) // 如果有token就转向todolist不返回登录页<br>    }<br>    next(); // 否则跳转回登录页<br>  }else{<br>    if(token != ‘null’ &amp;&amp; token != null){<br>      next() // 如果有token就正常转向<br>    }else{<br>      next(‘/‘) // 否则跳转回登录页<br>    }<br>  }<br>})</p>
<p>const app = new Vue({…}) // 省略<br>注意：一定要确保要调用 next() 方法，否则钩子就不会被 resolved。如果纯粹调用next(path)这样的方法最终还是会回到.beforeEach()这个钩子里面来，如果没有写对条件就有可能出现死循环，栈溢出的情况。</p>
<p>然后我们就可以看到如下效果：</p>
<p>login2todolist<br>login2todolist</p>
<p>Tips：这种只判断token存不存在就通过的验证是很不安全的，此例只是做了一个演示，实际上还应该进行更深一层的判断，比如从token解包出来的信息里包含我们想要的信息才可以作为有效token，才可以登录。等等。本文只是做一个简要介绍。</p>
<p>解析token<br>注意到我们在签发token的时候，写过这样几句话：</p>
<p>// server/controllers/user.js</p>
<p>// …</p>
<p>const userToken = {<br>  name: userInfo.user_name,<br>  id: userInfo.id<br>}<br>const secret = ‘vue-koa-demo’; // 指定密钥，这是之后用来判断token合法性的标志<br>const token = jwt.sign(userToken,secret); // 签发token</p>
<p>// …<br>我们将用户名和id打包进JWT的主体部分，同时我们解密的密钥是vue-koa-demo。所以我们可以通过这个信息，来进行登录后的用户名显示，以及用来区别这个用户是谁，这个用户有哪些Todolist。</p>
<p>接下来在Todolist页面进行token解析，从而让用户名显示为登录用户名。</p>
<p>注意： 前端直接暴露secret-key的做法其实并不安全。正确的做法应该是把token跟用户名和其他不是很重要的信息一起传过来，token只用于验证，而其他信息作为返回值正常返回。这样就不会暴露secret-key了。当然本文只是为了方便说明，给出的一个不恰当的获取用户信息的例子。</p>
<p>// src/components/TodoList.vue</p>
<p>// …</p>
<p>import jwt from ‘jsonwebtoken’ // 我们安装koa-jwt的时候会自动下载这个依赖</p>
<p>export default {</p>
<p>  created(){ // 组件创建时调用<br>    const userInfo = this.getUserInfo(); // 新增一个获取用户信息的方法<br>    if(userInfo != null){<br>      this.id = userInfo.id;<br>      this.name = userInfo.name;<br>    }else{<br>      this.id = ‘’;<br>      this.name = ‘’<br>    }<br>  },</p>
<p>  data () {<br>    return {<br>      name: ‘’, // 用户名改为空<br>      todos: ‘’,<br>      activeName: ‘first’,<br>      list:[],<br>      count: 0,<br>      id: ‘’ // 新增用户id属性，用于区别用户<br>    };<br>  },<br>  computed: {…}, //省略</p>
<p>  methods: {<br>    addTodos() {…}, // 省略<br>    finished(index) {…},<br>    remove(index) {…},<br>    restore(index) {…},<br>    getUserInfo(){ // 获取用户信息<br>      const token = sessionStorage.getItem(‘demo-token’);<br>      if(token != null &amp;&amp; token != ‘null’){<br>        let decode = jwt.decode(token); // 解析token<br>        return decode // decode解析出来实际上就是{name: XXX,id: XXX}<br>      }else {<br>        return null<br>      }<br>    }<br>  }<br>};</p>
<p>// …<br>于是你就可以看到：</p>
<p>todolist<br>todolist</p>
<p>用户名已经不是我们之前默认的Molunerfinn而是登录名molunerfinn了。</p>
<hr>
<h1 id="Todolist-增删改查的实现"><a href="#Todolist-增删改查的实现" class="headerlink" title="Todolist 增删改查的实现"></a><strong>Todolist 增删改查的实现</strong></h1><p>这个部分就是前后端协作了。我们要实现之前在纯前端部分实现的内容。我以最基本的两个方法来举例子：获取Todolist以及增加Todolist，剩下其实思路大同小异，我就提供代码和注释了，我相信也很容易懂。</p>
<p>Token的发送<br>之前说了，用JSON-WEB-TOKEN之后，这个系统的验证就完全依靠token了。如果token正确就下发资源，如果资源不正确，就返回错误信息。</p>
<p>因为我们用了koa-jwt，所以只需要在每条请求头上加上Authorization属性，值是Bearer {token值}，然后让Koa在接收请求之前验证一下token即可。但是如果每发一条请求就要手动写一句这个，太累了。于是我们可以做到全局Header设定。</p>
<p>打开src/main.js，在路由跳转的钩子里加上这句：</p>
<p>// scr/main.json</p>
<p>router.beforeEach((to,from,next) =&gt;{<br>  const token = sessionStorage.getItem(‘demo-token’);<br>  if(to.path == ‘/‘){<br>    if(token != ‘null’ &amp;&amp; token != null){<br>      next(‘/todolist’)<br>    }<br>    next();<br>  }else{<br>    if(token != ‘null’ &amp;&amp; token != null){<br>      Vue.prototype.$http.defaults.headers.common[‘Authorization’] = ‘Bearer ‘ + token; // 全局设定header的token验证，注意Bearer后有个空格<br>      next()<br>    }else{<br>      next(‘/‘)<br>    }<br>  }<br>})<br>这样就完成了token的客户端发送设定。</p>
<p>Koa端对Token的验证<br>接着我们实现两个简单的api，这两个api请求的路径就不是/auth/xxx而是/api/xxx了。我们还需要实现，访问/api/<em>路径的请求都需要经过koa-jwt的验证，而/auth/</em>的请求不需要。</p>
<p>首先去models目录下新建一个todolist.js的文件：</p>
<p>// server/models/todolist.js</p>
<p>const db = require(‘../config/db.js’),<br>      todoModel = ‘../schema/list.js’; // 引入todolist的表结构<br>const TodolistDb = db.Todolist; // 引入数据库</p>
<p>const Todolist = TodolistDb.import(todoModel); </p>
<p>const getTodolistById = function* (id){  // 获取某个用户的全部todolist<br>  const todolist = yield Todolist.findAll({ // 查找全部的todolist<br>    where: {<br>      user_id: id<br>    },<br>    attributes: [‘id’,’content’,’status’] // 只需返回这三个字段的结果即可<br>  });</p>
<p>  return todolist // 返回数据<br>}</p>
<p>const createTodolist = function* (data){ // 给某个用户创建一条todolist<br>  yield Todolist.create({<br>    user_id: data.id, // 用户的id，用来确定给哪个用户创建<br>    content: data.content,<br>    status: data.status<br>  })<br>  return true<br>}</p>
<p>module.exports = {<br>  getTodolistById,<br>  createTodolist<br>}<br>接着去controllers目录下新建一个todolist.js的文件：</p>
<p>// server/controllers/todolist</p>
<p>const todolist = require(‘../models/todolist.js’);</p>
<p>const getTodolist = function* (){ // 获取某个用户的所有todolist<br>  const id = this.params.id; // 获取url里传过来的参数里的id<br>  const result = yield todolist.getTodolistById(id);  // 通过yield “同步”地返回查询结果<br>  this.body = result // 将请求的结果放到response的body里返回<br>}</p>
<p>const createTodolist = function* (){ // 给某个用户创建一条todolist<br>  const data = this.request.body; // post请求，数据是在request.body里的<br>  const result = yield todolist.createTodolist(data);</p>
<p>  this.body = {<br>    success: true<br>  }<br>}</p>
<p>module.exports = {<br>  getTodolist,<br>  createTodolist<br>}<br>然后去routes文件夹里新建一个api.js文件：</p>
<p>// server/routes/api.js</p>
<p>const todolist = require(‘../controllers/todolist.js’);<br>const router = require(‘koa-router’)();</p>
<p>todolist(router); // 引入koa-router</p>
<p>module.exports = router; // 导出router规则<br>最后，去根目录下的app.js，给koa加上新的路由规则：</p>
<p>// app.js</p>
<p>const app = require(‘koa’)()<br>  , koa = require(‘koa-router’)()<br>  , json = require(‘koa-json’)<br>  , logger = require(‘koa-logger’)<br>  , auth = require(‘./server/routes/auth.js’)<br>  , api = require(‘./server/routes/api.js’)<br>  , jwt = require(‘koa-jwt’);</p>
<p>// ….. 省略</p>
<p>app.use(function* (next){<br>  let start = new Date;<br>  yield next;<br>  let ms = new Date - start;<br>  console.log(‘%s %s - %s’, this.method, this.url, ms);<br>});</p>
<p>app.use(function *(next){  //  如果JWT验证失败，返回验证失败信息<br>  try {<br>    yield next;<br>  } catch (err) {<br>    if (401 == err.status) {<br>      this.status = 401;<br>      this.body = {<br>        success: false,<br>        token: null,<br>        info: ‘Protected resource, use Authorization header to get access’<br>      };<br>    } else {<br>      throw err;<br>    }<br>  }<br>});</p>
<p>app.on(‘error’, function(err, ctx){<br>  console.log(‘server error’, err);<br>});</p>
<p>koa.use(‘/auth’, auth.routes()); // 挂载到koa-router上，同时会让所有的auth的请求路径前面加上’/auth’的请求路径。<br>koa.use(“/api”,jwt({secret: ‘vue-koa-demo’}),api.routes()) // 所有走/api/打头的请求都需要经过jwt中间件的验证。secret密钥必须跟我们当初签发的secret一致</p>
<p>app.use(koa.routes()); // 将路由规则挂载到Koa上。</p>
<p>// …省略<br>至此，后端的两个api已经构建完成。</p>
<p>初始化配置相对复杂一些，涉及到model、controllers、routes和app.js，可能会让人望而却步。实际上第一次构建完成之后，后续要添加api，基本上只需要在model和controllers写好方法，定好接口即可，十分方便。</p>
<p>前端对接接口<br>后端接口已经开放，接下来要把前端和后端进行对接。主要有两个对接接口：</p>
<p>获取某个用户的所有todolist<br>创建某个用户的一条todolist<br>接下来就是改写Todolist.vue里的方法了：</p>
<p>// todolist.js</p>
<p>// … 省略</p>
<p>created(){<br>  const userInfo = this.getUserInfo();<br>  if(userInfo != null){<br>    this.id = userInfo.id;<br>    this.name = userInfo.name;<br>  }else{<br>    this.id = ‘’;<br>    this.name = ‘’<br>  }<br>  this.getTodolist(); // 新增：在组件创建时获取todolist<br>},</p>
<p>// … 省略</p>
<p>methods: {<br>  addTodos() {<br>    if(this.todos == ‘’)<br>      return<br>    let obj = {<br>      status: false,<br>      content: this.todos,<br>      id: this.id<br>    }<br>    this.$http.post(‘/api/todolist’, obj) // 新增创建请求<br>      .then((res) =&gt; {<br>        if(res.status == 200){ // 当返回的状态为200成功时<br>          this.$message({<br>            type: ‘success’,<br>            message: ‘创建成功！’<br>          })<br>          this.getTodolist(); // 获得最新的todolist<br>        }else{<br>          this.$message.error(‘创建失败！’) // 当返回不是200说明处理出问题<br>        }<br>      }, (err) =&gt; {<br>        this.$message.error(‘创建失败！’) // 当没有返回值说明服务端错误或者请求没发送出去<br>        console.log(err)<br>      })<br>    this.todos = ‘’; // 将当前todos清空<br>  },<br>  // … 省略一些方法<br>  getTodolist(){<br>    this.$http.get(‘/api/todolist/‘ + this.id) // 向后端发送获取todolist的请求<br>      .then((res) =&gt; {<br>        if(res.status == 200){<br>          this.list = res.data // 将获取的信息塞入实例里的list<br>        }else{<br>          this.$message.error(‘获取列表失败！’)<br>        }<br>      }, (err) =&gt; {<br>        this.$message.error(‘获取列表失败！’)<br>        console.log(err)<br>      })<br>  }<br>}<br>至此，前后端的部分已经完整构建。让我们来看看效果：</p>
<p>todolist<br>todolist</p>
<p>做到这一步的时候其实我们的应用已经基本完成了。最后的收尾工作，让我们来收一下。</p>
<p>原本的前端版本还有完成、删除、还原三种状态，其中完成和还原只是状态的切换（更新），所以可以算是一个api，然后就是删除是单独一个api。于是我们就能算是完成了增、删、改、查了。接下去的部分就提供代码就行，其实思路跟之前的是一样的，只不过操作的函数不一样罢了。</p>
<p>Todolist的改、删</p>
<p>// server/models/todolist.js</p>
<p>// …省略</p>
<p>const removeTodolist = function* (id,user_id){<br>  yield Todolist.destroy({<br>    where: {<br>      id,<br>      user_id<br>    }<br>  })<br>  return true<br>}</p>
<p>const updateTodolist = function* (id,user_id,status){<br>  yield Todolist.update(<br>    {<br>      status<br>    },<br>    {<br>      where: {<br>        id,<br>        user_id<br>      }<br>    }<br>  )<br>  return true<br>}</p>
<p>module.exports = {<br>  getTodolistById,<br>  createTodolist,<br>  removeTodolist,<br>  updateTodolist<br>}</p>
<p>// server/controllers/todolist.js</p>
<p>// … 省略</p>
<p>const removeTodolist = function* (){<br>  const id = this.params.id;<br>  const user_id = this.params.userId;<br>  const result = yield todolist.removeTodolist(id,user_id);</p>
<p>  this.body = {<br>    success: true<br>  }<br>}</p>
<p>const updateTodolist = function* (){<br>  const id = this.params.id;<br>  const user_id = this.params.userId;<br>  let status = this.params.status;<br>  status == ‘0’ ? status = true : status =  false;// 状态反转（更新）</p>
<p>  const result = yield todolist.updateTodolist(id,user_id,status);</p>
<p>  this.body = {<br>    success: true<br>  }<br>}</p>
<p>module.exports = (router) =&gt; {<br>  getTodolist,<br>  createTodolist,<br>  removeTodolist,<br>  updateTodolist<br>}<br> <!-- src/components/TodoList.vue --></p>
<p>….</p>
<!-- 把完成和还原的方法替换成了update -->
<p><el-button size="small" type="primary" @click="update(index)">完成</el-button><br>….</p>
<p><el-button size="small" type="primary" @click="update(index)">还原</el-button><br>….</p>
<p><script><br>// ….省略<br>  methods:{<br>    // … 省略<br>    update(index) {<br>      this.$http.put(‘/api/todolist/‘+ this.id + ‘/‘ + this.list[index].id + ‘/‘ + this.list[index].status)<br>        .then((res) =&gt; {<br>          if(res.status == 200){<br>            this.$message({<br>              type: ‘success’,<br>              message: ‘任务状态更新成功！’<br>            })<br>            this.getTodolist();<br>          }else{<br>            this.$message.error(‘任务状态更新失败！’)<br>          }<br>        }, (err) =&gt; {<br>          this.$message.error(‘任务状态更新失败！’)<br>          console.log(err)<br>        })<br>    },<br>    remove(index) {<br>      this.$http.delete(‘/api/todolist/‘+ this.id + ‘/‘ + this.list[index].id)<br>        .then((res) =&gt; {<br>          if(res.status == 200){<br>            this.$message({<br>              type: ‘success’,<br>              message: ‘任务删除成功！’<br>            })<br>            this.getTodolist();<br>          }else{<br>            this.$message.error(‘任务删除失败！’)<br>          }<br>        }, (err) =&gt; {<br>          this.$message.error(‘任务删除失败！’)<br>          console.log(err)<br>        })<br>    },<br>  }<br>// … 省略<br></script><br>….<br>让我们来看看最后99%成品的效果吧：</p>
<p>Todolist<br>Todolist</p>
<hr>
<h1 id="项目部署"><a href="#项目部署" class="headerlink" title="项目部署"></a><strong>项目部署</strong></h1><p>很多教程到类似于我上面的部分就结束了。但是实际上我们做一个项目最想要的就是部署给大家用不是么？</p>
<p>在部署这块有些坑，需要让大家也一起知道一下。这个项目是个全栈项目（虽然是个很简单的。。。），所以就涉及到前后端通信的问题，也就会涉及到是同域请求还是跨域请求。</p>
<p>我们也说过，要解决这个问题有两种方便的解决办法，第一种，服务端加上cors，客户端就可以随意的跨域请求。但是这样会有个问题，因为我们是以同域的形式开发，请求的地址也是写的相对地址：/api/<em>、auth/</em>这样的路径，访问的路径的自然是同域。如果要在服务端加上cors，我们还需要将我们的所有请求地址改成localhost:8889/api/<em>，localhost:8889/auth/</em>，这样的话，如果服务端的端口号一变，我们还需要重新修改前端所有的请求地址。这样很不方便也很不科学。</p>
<p>因此，要将请求变为同域才是最好的解决办法——不管服务端端口号怎么变，只要是同域都可以请求到。</p>
<p>于是要把Vue和Koa结合起来变成一个完整的项目（之前实际上都是在开发模式下，webpack帮我们进行请求的代理转发，所以看起来像是同域请求，而Vue和Koa并没有完全结合起来），就得在生产模式下，将Vue的静态文件交给Koa“托管”，所有访问前端的请求全部走Koa端，包括静态文件资源的请求，也走Koa端，把Koa作为一个Vue项目的静态资源服务器，这样就可以让Vue里的请求走的都是同域了。（相当于，之前开发模式是webpack开启了一个服务器托管了Vue的资源和请求，现在生产模式下改成Koa托管Vue的资源和请求）</p>
<p>要在开发和生产模式改变不同的托管服务器，其实也很简单，只需要在生产模式下，用Koa的静态资源服务中间件托管构建好的Vue文件即可。</p>
<p>Webpack打包<br>部署之前我们要用Webpack将我们的前端项目打包输出一下。但是如果直接用npm run build，你会发现打包出来的文件太大了：</p>
<pre><code>                                              Asset       Size  Chunks             Chunk Names
static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css     120 kB    2, 0  [emitted]  app
             static/fonts/element-icons.a61be9c.eot    13.5 kB          [emitted]
               static/img/element-icons.09162bc.svg    17.4 kB          [emitted]
         static/js/manifest.8ea250834bdc80e4d73b.js  832 bytes       0  [emitted]  manifest
           static/js/vendor.75bbe7ecea37b0d4c62d.js     623 kB    1, 0  [emitted]  vendor
              static/js/app.e2d125562bfc4c57f9cb.js    16.5 kB    2, 0  [emitted]  app
             static/fonts/element-icons.b02bdc1.ttf    13.2 kB          [emitted]
     static/js/manifest.8ea250834bdc80e4d73b.js.map    8.86 kB       0  [emitted]  manifest
       static/js/vendor.75bbe7ecea37b0d4c62d.js.map    3.94 MB    1, 0  [emitted]  vendor
          static/js/app.e2d125562bfc4c57f9cb.js.map    64.8 kB    2, 0  [emitted]  app
</code></pre><p>static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css.map     151 kB    2, 0  [emitted]  app<br>                                             index.html  563 bytes          [emitted]<br>竟然有3.94MB的map文件。这肯定是不能接受的。于是要修改一下webpack的输出的设置，取消输出map文件。</p>
<p>找到根目录下的config/index.js：把productionSourceMap: true这句话改成productionSourceMap: false。然后再执行一遍npm run build。</p>
<pre><code>                                     Asset       Size  Chunks             Chunk Names
    static/fonts/element-icons.a61be9c.eot    13.5 kB          [emitted]
    static/fonts/element-icons.b02bdc1.ttf    13.2 kB          [emitted]
      static/img/element-icons.09162bc.svg    17.4 kB          [emitted]
static/js/manifest.3ba218c80028a707a728.js  774 bytes       0  [emitted]  manifest
  static/js/vendor.75bbe7ecea37b0d4c62d.js     623 kB    1, 0  [emitted]  vendor
     static/js/app.b6acaca2531fc0baa447.js    16.5 kB    2, 0  [emitted]  app
</code></pre><p>static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css     120 kB    2, 0  [emitted]  app<br>                                         index.html  563 bytes          [emitted]<br>把sourceMap去掉了之后，体积就小下来了。虽然600+kb的大小还是有点大，不过放到服务端，gzip之后只剩150+kb的体积勉强还是可以接受。当然，对于webpack输出的优化，不是本文讨论的范围，有很多更好的文章讲述了这个东西，故本文不再详细展开。</p>
<p>打包好后就是相当于输出了一堆静态文件，当然这堆静态文件需要放在服务端才可以访问。我们要将这堆静态资源用Koa托管。</p>
<p>Koa serve静态资源<br>yarn add koa-static</p>
<p>打开app.js，引入两个新依赖，其中path是nodejs原生自带。</p>
<p>// app.js</p>
<p>// ….<br>const path =require(‘path’)<br>    , serve = require(‘koa-static’);<br>// ….</p>
<p>// 静态文件serve在koa-router的其他规则之上<br>app.use(serve(path.resolve(‘dist’))); // 将webpack打包好的项目目录作为Koa静态文件服务的目录</p>
<p>// 下面这些是之前就有的。。。为了方便找位置故标示出来<br>koa.use(‘/auth’, auth.routes());<br>koa.use(“/api”,jwt({secret: ‘vue-koa-demo’}),api.routes()) </p>
<p>// …<br>然后重新运行一遍node app.js，看到输出Koa is listening in 8889后，你可以打开浏览器localhost:8889就可以看到如下情景：</p>
<p>vue-koa<br>vue-koa</p>
<p>至此已经基本上接近尾声，不过还存在一个问题：如果我们登录进去之后，在todolist页面一刷新，就会出现：</p>
<p>404<br>404</p>
<p>为什么会出现这种情况？简单来说是因为我们使用了前端路由，用了HTML5 的History模式，如果没有做其他任何配置的话，刷新页面，那么浏览器将会去服务端访问这个页面地址，因为服务端并没有配置这个地址的路由，所以自然就返回404 Not Found了。</p>
<p>详细可以参考vue-router的这篇文档</p>
<p>该怎么解决？其实也很简单，多加一个中间件：koa-history-api-fallback即可.</p>
<p>yarn add koa-history-api-fallback</p>
<p>//… 省略</p>
<p>const historyApiFallback = require(‘koa-history-api-fallback’); // 引入依赖</p>
<p>app.use(require(‘koa-bodyparser’)());<br>app.use(json());<br>app.use(logger());<br>app.use(historyApiFallback()); // 在这个地方加入。一定要加在静态文件的serve之前，否则会失效。</p>
<p>// …<br>这个时候，你再重新启动一下koa，登录之后再刷新页面，就不会再出现404 Not Found了。</p>
<p>API Test<br>本来写到上面基本本文已经算是结束了。但是由于我在开发的过程中遇到了一些问题，所以还需要做一些微调。</p>
<p>我们知道koa的use方法是有顺序只差的。</p>
<p>const app = require(‘koa’);<br>app.use(A);<br>app.use(B);<br>const app = require(‘koa’);<br>app.use(B);<br>app.use(A);<br>这二者是有区别的，谁先被use，谁的规则就放到前面先执行。</p>
<p>因此如果我们将静态文件的serve以及historyApiFallback放在了api的请求之前，那么用postman测试api的时候总会先返回完整的页面：</p>
<p>postman<br>postman</p>
<p>因此正确的做法，应该是将它们放到我们写的api的规则之后：</p>
<p>// app.js<br>// …</p>
<p>koa.use(‘/auth’, auth.routes()); // 挂载到koa-router上，同时会让所有的auth的请求路径前面加上’/auth’的请求路径。<br>koa.use(“/api”,jwt({secret: ‘vue-koa-demo’}),api.routes()) // 所有走/api/打头的请求都需要经过jwt验证。</p>
<p>app.use(koa.routes()); // 将路由规则挂载到Koa上。</p>
<p>app.use(historyApiFallback()); // 将这两个中间件挂载在api的路由之后<br>app.use(serve(path.resolve(‘dist’))); // 将webpack打包好的项目目录作为Koa静态文件服务的目录<br>这样就能正常返回数据了。</p>
<p>Nginx配置<br>真正部署到服务器的时候，我们肯定不会让大家输入域名:8889这样的方式让大家访问。所以需要用Nginx监听80端口，把访问我们指定域名的请求引导转发给Koa服务端。</p>
<p>大致的nginx.conf如下：</p>
<p>http {</p>
<h1 id="…"><a href="#…" class="headerlink" title="…."></a>….</h1><p>  upstream koa.server{<br>    server 127.0.0.1:8889;<br>  }</p>
<p>  server {<br>    listen   80;<br>    server_name xxx.xxx.com;</p>
<pre><code>location / {
  proxy_pass http://koa.server;
  proxy_redirect off;
}

#....
</code></pre><p>  }</p>
<p>  #….<br>}<br>如果有精力还可以配置一下Nginx的Gzip，能让请求的JS\CSS\HTML等静态文件更小，响应速度更快些。</p>
<hr>
<h1 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a><strong>写在最后</strong></h1><p>至此，我们已经完成了一个从前端到后端，从本地到服务器的完整项目。虽然它真的是个很简单的小东西，被大家也已经用其他的方式写烂了（比如用localStorage做存储）。但是它作为一个完整的前后端的DEMO，我觉得让大家入门也相对更容易一些，能够体会到全栈开发也不是想象中的“那么难”（入门的难度还是可以接受的嘛）。有了Nodejs之后我们能够做的事真的好多！</p>
<p>当然，由于篇幅有限，本文能够讲述东西毕竟不够多，而且讲的东西也不可能面面俱到，很多东西都是点到即止，让大家能够自己发挥。其实还想讲讲Event Bus的简单使用，还有分页的基本实现等等，东西太多了，一时间大家消化不了。</p>
<p>实际上我在做前段时间的项目的时候，也是完全不知道怎么把Vue和Koa结合起来开发。我甚至不知道怎么用Koa来提供API，我只会用Koa来做服务端渲染，比如那些JADE\EJS等模板引擎渲染的页面。所以之前那个项目做完让我自己学到良多东西，故而也分享给大家。</p>
<p>实际上本文的Koa的api提供的形式也尽量和RESTful靠拢了，因此你也可以学会如何通过Koa提供RESTful形式的API了。</p>
<p>最后放上本文项目的Github地址，如果这个项目对你有帮助，希望大家可以fork，给我提建议，如果再有时间，可以点个Star那就更好啦~</p>
<p>另外，本文的版本是用Koa1写成的。仓库已经更新Koa2。从Koa1-&gt;Koa2并没有什么难度，其实很关键的两点是：</p>
<p>用async await替代yield generation<br>用koa2的中间件替代koa1的中间件，原因同上一条<br>互相学习，如果能从这个项目里学到东西我就很开心啦~</p>


                <hr>

                

                <ul class="pager">
                    
                    
                        <li class="next">
                            <a href="/myblog/2018/11/27/hello-world/" data-toggle="tooltip" data-placement="top" title="你好 Hexo">Next Post &rarr;</a>
                        </li>
                    
                </ul>

                

                
                <!-- disqus 评论框 start -->
                <div class="comment">
                    <div id="disqus_thread" class="disqus-thread"></div>
                </div>
                <!-- disqus 评论框 end -->
                

            </div>
    <!-- Side Catalog Container -->
        

    <!-- Sidebar Container -->

            <div class="
                col-lg-8 col-lg-offset-2
                col-md-10 col-md-offset-1
                sidebar-container">

                <!-- Featured Tags -->
                
                <section>
                    <!-- no hr -->
                    <h5><a href="/myblog/tags/">FEATURED TAGS</a></h5>
                    <div class="tags">
                       
                    </div>
                </section>
                

                <!-- Friends Blog -->
                
                <hr>
                <h5>FRIENDS</h5>
                <ul class="list-inline">

                    
                        <li><a href="https://inherents.gitee.io/myblog/" target="_blank">inherent&#39;s Blog</a></li>
                    
                        <li><a href="https://docs.djangoproject.com" target="_blank">Django</a></li>
                    
                        <li><a href="http://mint-ui.github.io/docs/#/zh-cn2" target="_blank">Mint-UI</a></li>
                    
                        <li><a href="http://element-cn.eleme.io/#/zh-CN/component/installation" target="_blank">Element-UI</a></li>
                    
                        <li><a href="#" target="_blank">My Resume</a></li>
                    
                        <li><a href="#" target="_blank">Others</a></li>
                    
                </ul>
                
            </div>

        </div>
    </div>
</article>




<!-- disqus 公共JS代码 start (一个网页只需插入一次) -->
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = "hexo-theme-huxblog";
    var disqus_identifier = "https://inherents.gitee.io/myblog/2018/11/27/Vue2-Koa2/";
    var disqus_url = "https://inherents.gitee.io/myblog/2018/11/27/Vue2-Koa2/";

    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<!-- disqus 公共JS代码 end -->




<!-- async load function -->
<script>
    function async(u, c) {
      var d = document, t = 'script',
          o = d.createElement(t),
          s = d.getElementsByTagName(t)[0];
      o.src = u;
      if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
      s.parentNode.insertBefore(o, s);
    }
</script>
<!-- anchor-js, Doc:http://bryanbraun.github.io/anchorjs/ -->
<script>
    async("https://cdn.bootcss.com/anchor-js/1.1.1/anchor.min.js",function(){
        anchors.options = {
          visible: 'always',
          placement: 'right',
          icon: '#'
        };
        anchors.add().remove('.intro-header h1').remove('.subheading').remove('.sidebar-container h5');
    })
</script>
<style>
    /* place left on bigger screen */
    @media all and (min-width: 800px) {
        .anchorjs-link{
            position: absolute;
            left: -0.75em;
            font-size: 1.1em;
            margin-top : -0.1em;
        }
    }
</style>



    <!-- Footer -->
    <!-- Footer -->
<footer>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <ul class="list-inline text-center">
                
                
                    <li>
                        <a target="_blank" href="https://twitter.com/Demonbane_x">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                
                
                    <li>
                        <a target="_blank" href="https://www.zhihu.com/people/Demonbane">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa  fa-stack-1x fa-inverse">知</i>
                            </span>
                        </a>
                    </li>
                

                
                    <li>
                        <a target="_blank" href="http://weibo.com/Demonbane">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-weibo fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                

                
                    <li>
                        <a target="_blank" href="https://www.facebook.com/demonbane.cn">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-facebook fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                

                
                    <li>
                        <a target="_blank" href="https://github.com/Kaijun">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-github fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                

                
                    <li>
                        <a target="_blank" href="https://www.linkedin.com/in/kaijun-chen-42089354">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-linkedin fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                

                </ul>
                <p class="copyright text-muted">
                    Copyright &copy; 郝以利 2018 
                    <br>
                    Theme by <a href="http://huangxuan.me">Hux</a> 
                    <span style="display: inline-block; margin: 0 5px;">
                        <i class="fa fa-heart"></i>
                    </span> 
                    Ported by <a href="http://blog.kaijun.rocks">Kaijun</a> | 
                    <iframe style="margin-left: 2px; margin-bottom:-5px;" frameborder="0" scrolling="0" width="91px" height="20px" src="https://ghbtns.com/github-btn.html?user=kaijun&repo=hexo-theme-huxblog&type=star&count=true">
                    </iframe>
                </p>
            </div>
        </div>
    </div>
</footer>

<!-- jQuery -->
<script src="/myblog/js/jquery.min.js"></script>

<!-- Bootstrap Core JavaScript -->
<script src="/myblog/js/bootstrap.min.js"></script>

<!-- Custom Theme JavaScript -->
<script src="/myblog/js/hux-blog.min.js"></script>


<!-- async load function -->
<script>
    function async(u, c) {
      var d = document, t = 'script',
          o = d.createElement(t),
          s = d.getElementsByTagName(t)[0];
      o.src = u;
      if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
      s.parentNode.insertBefore(o, s);
    }
</script>

<!-- 
     Because of the native support for backtick-style fenced code blocks 
     right within the Markdown is landed in Github Pages, 
     From V1.6, There is no need for Highlight.js, 
     so Huxblog drops it officially.

     - https://github.com/blog/2100-github-pages-now-faster-and-simpler-with-jekyll-3-0  
     - https://help.github.com/articles/creating-and-highlighting-code-blocks/    
-->
<!--
    <script>
        async("http://cdn.bootcss.com/highlight.js/8.6/highlight.min.js", function(){
            hljs.initHighlightingOnLoad();
        })
    </script>
    <link href="http://cdn.bootcss.com/highlight.js/8.6/styles/github.min.css" rel="stylesheet">
-->


<!-- jquery.tagcloud.js -->
<script>
    // only load tagcloud.js in tag.html
    if($('#tag_cloud').length !== 0){
        async("https://inherents.gitee.io/myblog/myblog/js/jquery.tagcloud.js",function(){
            $.fn.tagcloud.defaults = {
                //size: {start: 1, end: 1, unit: 'em'},
                color: {start: '#bbbbee', end: '#0085a1'},
            };
            $('#tag_cloud a').tagcloud();
        })
    }
</script>

<!--fastClick.js -->
<script>
    async("https://cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js", function(){
        var $nav = document.querySelector("nav");
        if($nav) FastClick.attach($nav);
    })
</script>


<!-- Google Analytics -->


<script>
    // dynamic User by Hux
    var _gaId = 'UA-49627206-1';
    var _gaDomain = 'huangxuan.me';

    // Originial
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', _gaId, _gaDomain);
    ga('send', 'pageview');
</script>




<!-- Baidu Tongji -->

<script>
    // dynamic User by Hux
    var _baId = '4cc1f2d8f3067386cc5cdb626a202900';

    // Originial
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "//hm.baidu.com/hm.js?" + _baId;
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
</script>


<!-- Side Catalog -->





<!-- Image to hack wechat -->
<img src="https://inherents.gitee.io/myblog/myblog/img/icon_wechat.png" width="0" height="0" />
<!-- Migrate from head to bottom, no longer block render and still work -->

</body>

</html>
